Java中Class对象详解

您所在的位置:网站首页 class 对象 Java中Class对象详解

Java中Class对象详解

2023-12-31 09:12| 来源: 网络整理| 查看: 265

Class类简介

  在java世界里,一切皆对象。从某种意义上来说,java有两种对象:实例对象和Class对象。每个类的运行时的类型信息就是用Class对象表示的。它包含了与类有关的信息。其实我们的实例对象就通过Class对象来创建的。Java使用Class对象执行其RTTI(运行时类型识别,Run-Time Type Identification),多态是基于RTTI实现的。

  每一个类都有一个Class对象,每当编译一个新类就产生一个Class对象,基本类型 (boolean, byte, char, short, int, long, float, and double)有Class对象,数组有Class对象,就连关键字void也有Class对象(void.class)。Class对象对应着java.lang.Class类,如果说类是对象抽象和集合的话,那么Class类就是对类的抽象和集合。

  Class类没有公共的构造方法,Class对象是在类加载的时候由Java虚拟机以及通过调用类加载器中的 defineClass 方法自动构造的,因此不能显式地声明一个Class对象。一个类被加载到内存并供我们使用需要经历如下三个阶段:

加载,这是由类加载器(ClassLoader)执行的。通过一个类的全限定名来获取其定义的二进制字节流(Class字节码),将这个字节流所代表的静态存储结构转化为方法去的运行时数据接口,根据字节码在java堆中生成一个代表这个类的java.lang.Class对象。

链接。在链接阶段将验证Class文件中的字节流包含的信息是否符合当前虚拟机的要求,为静态域分配存储空间并设置类变量的初始值(默认的零值),并且如果必需的话,将常量池中的符号引用转化为直接引用。

初始化。到了此阶段,才真正开始执行类中定义的java程序代码。用于执行该类的静态初始器和静态初始块,如果该类有父类的话,则优先对其父类进行初始化。

    所有的类都是在对其第一次使用时,动态加载到JVM中的(懒加载)。当程序创建第一个对类的静态成员的引用时,就会加载这个类。使用new创建类对象的时候也会被当作对类的静态成员的引用。因此java程序程序在它开始运行之前并非被完全加载,其各个类都是在必需时才加载的。这一点与许多传统语言都不同。动态加载使能的行为,在诸如C++这样的静态加载语言中是很难或者根本不可能复制的。

  在类加载阶段,类加载器首先检查这个类的Class对象是否已经被加载。如果尚未加载,默认的类加载器就会根据类的全限定名查找.class文件。在这个类的字节码被加载时,它们会接受验证,以确保其没有被破坏,并且不包含不良java代码。一旦某个类的Class对象被载入内存,我们就可以它来创建这个类的所有对象。

如何获得Class对象

有三种获得Class对象的方式:

Class.forName(“类的全限定名”)实例对象.getClass()类名.class (类字面常量) Class.forName 和getClass()

我们先看看如下的例子:

package com.cry; class Dog { static { System.out.println("Loading Dog"); } } class Cat { static { System.out.println("Loading Cat"); } } public class Test { public static void main(String[] args){ System.out.println("inside main"); new Dog(); System.out.println("after creating Dog"); try { Class cat=Class.forName("com.cry.Cat"); } catch (ClassNotFoundException e) { System.out.println("Couldn't find Cat"); } System.out.println("finish main"); } } /* Output: inside main Loading Dog after creating Dog Loading Cat finish main */

  上面的Dog、Cat类中都有一个静态语句块,该语句块在类第一次被加载时候被执行。这时会有相应的信息打印出来,告诉我们这个类什么时候被加载了。从输出中可以看到,Class对象仅在需要的时候才被加载,static初始化是在类加载时进行的。

  Class.forName方法是Class类的一个静态成员。forName在执行的过程中发现如果类Dog还没有被加载,那么JVM就会调用类加载器去加载Dog类,并返回加载后的Class对象。Class对象和其他对象一样,我们可以获取并操作它的引用。在类加载的过程中,Dog类的静态语句块会被执行。如果Class .forName找不到你要加载的类,它会抛出ClassNotFoundException异常。

  Class.forName的好处就在于,不需要为了获得Class引用而持有该类型的对象,只要通过全限定名就可以返回该类型的一个Class引用。如果你已经有了该类型的对象,那么我们就可以通过调用getClass()方法来获取Class引用了,这个方法属于根类Object的一部分,它返回的是表示该对象的实际类型的Class引用:

package com.cry; class Dog { static { System.out.println("Loading Dog"); } } public class Test { public static void main(String[] args) { System.out.println("inside main"); Dog d = new Dog(); System.out.println("after creating Dog"); Class c = d.getClass(); System.out.println("finish main"); } } /* Output: inside main Loading Dog after creating Dog finish main */

利用new操作符创建对象后,类已经装载到内存中了,所以执行getClass()方法的时候,就不会再去执行类加载的操作了,而是直接从java堆中返回该类型的Class引用。

类字面常量

  java还提供了另一种方法来生成对Class对象的引用。即使用类字面常量,就像这样:Cat.class,这样做不仅更简单,而且更安全,因为它在编译时就会受到检查(因此不需要置于try语句块中)。并且根除了对forName()方法的调用,所有也更高效。类字面量不仅可以应用于普通的类,也可以应用于接口、数组及基本数据类型。

注意:基本数据类型的Class对象和包装类的Class对象是不一样的:

Class c1 = Integer.class; Class c2 = int.class; System.out.println(c1); System.out.println(c2); System.out.println(c1 == c2); /* Output class java.lang.Integer int false */

但是在包装类中有个一个字段TYPE,TYPE字段是一个引用,指向对应的基本数据类型的Class对象,如下所示,左右两边相互等价: 这里写图片描述     用.class来创建对Class对象的引用时,不会自动地初始化该Class对象(这点和Class.forName方法不同)。类对象的初始化阶段被延迟到了对静态方法或者非常数静态域首次引用时才执行:

package com.cry; class Dog { static final String s1 = "Dog_s1"; static String s2 = "Dog_s2"; static { System.out.println("Loading Dog"); } } class Cat { static String s1 = "Cat_s1"; static { System.out.println("Loading Cat"); } } public class Test { public static void main(String[] args) throws ClassNotFoundException { System.out.println("----Star Dog----"); Class dog = Dog.class; System.out.println("------"); System.out.println(Dog.s1); System.out.println("------"); System.out.println(Dog.s2); System.out.println("---start Cat---"); Class cat = Class.forName("com.cry.Cat"); System.out.println("-------"); System.out.println(Cat.s1); System.out.println("finish main"); } } /* Output: ----Star Dog---- ------ Dog_s1 ------ Loading Dog Dog_s2 ---start Cat--- Loading Cat ------- Cat_s1 finish main */

  从上面我们可以看到,如果仅使用.class语法来获得对类的Class引用是不会引发初始化的。但是如果使用Class.forName来产生引用,就会立即进行了初始化,就像Cat所看到的。

  如果一个字段被static final修饰,我们称为”编译时常量“,就像Dog的s1字段那样,那么在调用这个字段的时候是不会对Dog类进行初始化的。因为被static和final修饰的字段,在编译期就把结果放入了常量池中了。但是,如果只是将一个域设置为static 或final的,还不足以确保这种行为,就如调用Dog的s2字段后,会强制Dog进行类的初始化,因为s2字段不是一个编译时常量。

通过javap -c -v对Dog的字节码进行反汇编:

{ static final java.lang.String s1; flags: ACC_STATIC, ACC_FINAL ConstantValue: String Dog_s1 static java.lang.String s2; flags: ACC_STATIC com.cry.Dog(); flags: Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."":()V 4: return LineNumberTable: line 3: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lcom/cry/Dog; static {}; flags: ACC_STATIC Code: stack=2, locals=0, args_size=0 0: ldc #2 // String Dog_s2 2: putstatic #3 // Field s2:Ljava/lang/String; 5: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream; 8: ldc #5 // String Loading Dog 10: invokevirtual #6 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 13: return LineNumberTable: line 6: 0 line 9: 5 line 10: 13 }

  从上面可以看出s1在编译后被ConstantValue属性修饰 ConstantValue: String Dog_s1,表示即同时被final和static修饰。而s2并没有被ConstantValue修饰,因为它不是一个编译时常量。在static{}中表示类的初始化操作,在操作中我们看到只有s2字段进行了赋值,而却没有s1的踪影,因此调用s1字段是不会触发类的初始化的。

小结

  一旦类被加载了到了内存中,那么不论通过哪种方式获得该类的Class对象,它们返回的都是指向同一个java堆地址上的Class引用。jvm不会创建两个相同类型的Class对象:

package com.cry; class Cat { static { System.out.println("Loading Cat"); } } public class Test { public static void main(String[] args) throws ClassNotFoundException { System.out.println("inside main"); Class c1 = Cat.class; Class c2= Class.forName("com.cry.Cat"); Class c3=new Cat().getClass(); Class c4 =new Cat().getClass(); System.out.println(c1==c2); System.out.println(c2==c3); System.out.println("finish main"); } } /* Output: inside main ------- Loading Cat true true finish main */

从上面我们可以看出执行不同获取Class引用的方法,返回的其实都是同一个Class对象。

  其实对于任意一个Class对象,都需要由它的类加载器和这个类本身一同确定其在就Java虚拟机中的唯一性,也就是说,即使两个Class对象来源于同一个Class文件,只要加载它们的类加载器不同,那这两个Class对象就必定不相等。这里的“相等”包括了代表类的Class对象的equals()、isAssignableFrom()、isInstance()等方法的返回结果,也包括了使用instanceof关键字对对象所属关系的判定结果。所以在java虚拟机中使用双亲委派模型来组织类加载器之间的关系,来保证Class对象的唯一性。

泛型Class引用

  Class引用表示的就是它所指向的对象的确切类型,而该对象便是Class类的一个对象。在JavaSE5中,允许你对Class引用所指向的Class对象的类型进行限定,也就是说你可以对Class对象使用泛型语法。通过泛型语法,可以让编译器强制指向额外的类型检查:

public final class Class implements java.io.Serializable, GenericDeclaration, Type, AnnotatedElement { Class c1 = int.class; c1=Integer.class; //c1=Double.class; 编译报错

虽然int.class和Integer.class指向的不是同一个Class对象引用,但是它们基本类型和包装类的关系,int可以自动包装为Integer,所以编译器可以编译通过。

泛型中的类型可以持有其子类的引用吗?不行:

Class c1 = Integer.class; //编译报错

虽然Integer继承自Number,但是编译器无法编译通过。

为了使用泛化的Class引用放松限制,我们还可以使用通配符,它是Java泛型的一部分。通配符的符合是”?“,表示“任何事物“:

Class c1 = int.class; c1= double.class;

Class

Class


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3